package alibaba

import (
	"context"
	"fmt"
	"os"
	"os/exec"
	"strconv"
	"strings"

	"golang.aliyun-inc.com/agit/agit/proc-receive-hook/protocol"
)

func (c *CodeupCommandHandler) resolveAGitFlowParams(ctx context.Context, command *protocol.Command) (*AGitFlowParams, error) {
	params := &AGitFlowParams{
		UseSpaceX:   true,
		PushOpt:     "",
		NewOID:      command.NewOID,
		Description: new(string),
	}

	if err := params.resolveCommand(command); err != nil {
		return nil, fmt.Errorf("resolve agitflow params from command failed: %w", err)
	}

	// we need to resove source and target branches on MR creation
	if params.Action == "create" {
		if err := params.resolveSourceAndTargetBranch(command.Ref); err != nil {
			return nil, fmt.Errorf("resolve source and target branch failed: %w", err)
		}
	}

	if err := params.resolvePushOptions(*c.pushopts); err != nil {
		return nil, fmt.Errorf("resolve agitflow params from push options failed: %w", err)
	}

	if err := params.resolveRefOptions(command.Ref); err != nil {
		return nil, fmt.Errorf("resolve agitflow params from ref options failed: %w", err)
	}

	if err := params.resolveEnv(); err != nil {
		return nil, fmt.Errorf("resolve agitflow params from env failed: %w", err)
	}

	if params.UserID == "" && params.KeyID != "" {
		userInfo, err := c.api.QueryUser(ctx, params.KeyID)
		if err != nil {
			return nil, fmt.Errorf("query user by %q failed: %w", params.KeyID, err)
		}
		params.UserID = strconv.Itoa(userInfo.ID)
	}

	return params, nil
}

// resolve action and ID from command
func (params *AGitFlowParams) resolveCommand(command *protocol.Command) error {
	refSplits := strings.SplitN(command.Ref, "/", 3)
	if len(refSplits) != 3 {
		return fmt.Errorf("illegal ref %q for agitflow", command.Ref)
	}
	switch refSplits[1] {
	case "for-review":
		params.Action = "update"
		params.ID = strings.Split(refSplits[2], "%")[0]
	case "drafts":
		params.Action = "create"
		params.IsDraft = true
	case "for":
		params.Action = "create"
		params.IsDraft = false
	default:
		return fmt.Errorf("illegal ref %q for agitflow", command.Ref)
	}

	return nil
}

func (params *AGitFlowParams) resolvePushOptions(opts []protocol.PushOption) (err error) {
	for _, opt := range opts {
		switch strings.TrimSpace(opt.Name) {
		case "proc-receive-debug":
			if opt.Value == "1" {
				params.verbose = true
			}
		case "title":
			params.Title, err = unmarshal(opt.Value)
		case "description":
			*params.Description, err = unmarshal(opt.Value)
		case "message":
			var msg string
			msg, err = unmarshal(opt.Value)
			i := strings.IndexByte(msg, '\n')
			if i == -1 {
				params.Title = msg
			} else {
				params.Title = msg[:i]
				*params.Description = strings.TrimSpace(msg[i:])
			}
		case "reviewer", "reviewers", "re":
			var reviewer string
			reviewer, err = unmarshal(opt.Value)
			params.Reviewers = setOrAppendStrPointer(params.Reviewers, reviewer)
		case "cc":
			var cc string
			cc, err = unmarshal(opt.Value)
			params.CCs = setOrAppendStrPointer(params.CCs, cc)
		case "issues", "issue":
			var issue string
			issue, err = unmarshal(opt.Value)
			params.Issues = setOrAppend(params.Issues, issue)
		case "private":
			params.IsPrivate = opt.Value == "yes"
		case "wip":
			params.IsWIP = opt.Value == "yes"
			params.IsDraft = opt.Value == "yes"
		case "notify":
			params.IsNotify = opt.Value != "no"
		case "oldoid":
			params.OldOID = opt.Value
		}

		if err != nil {
			return
		}
	}
	return nil
}

func (params *AGitFlowParams) resolveRefOptions(ref string) error {
	refSplits := strings.SplitN(ref, "%", 2)
	if len(refSplits) == 2 {
		params.options = strings.Split(refSplits[1], ",")
	}

	for _, option := range params.options {
		switch {
		case strings.HasPrefix(option, "r="):
			params.Reviewers = setOrAppendStrPointer(params.Reviewers, option[2:])
		case strings.HasPrefix(option, "cc="):
			params.CCs = setOrAppendStrPointer(params.CCs, option[3:])
		case option == "private":
			params.IsPrivate = true
		case option == "wip":
			params.IsWIP = true
		case option == "notify=NONE":
			params.IsNotify = false
		case strings.HasPrefix(option, "topic="):
			if params.SourceBranch == "" {
				params.SourceBranch = option[6:]
			}
		case strings.HasPrefix(option, "oldoid="):
			params.OldOID = option[7:]
		case strings.HasPrefix(option, "title="):
			params.Title = option[6:]
		case strings.HasPrefix(option, "description="):
			*params.Description = option[12:]
		case strings.HasPrefix(option, "issue="):
			params.Issues = setOrAppend(params.Issues, option[6:])
		case option == "proc-receive-debug":
			params.verbose = true
		}
	}
	return nil
}

// resolveEnv: parse agitflow params from environment, including GL_ID, GL_REPOSITORY,
// GL_REPOSITORY_ID and GL_USERNAME
func (params *AGitFlowParams) resolveEnv() error {
	// parse gl_id from env
	params.GlID, _ = os.LookupEnv("GL_ID")
	glIDSplits := strings.SplitN(params.GlID, "-", 2)
	if len(glIDSplits) != 2 {
		return fmt.Errorf("invalid gl_id %q", params.GlID)
	}
	switch glIDSplits[0] {
	case "user":
		params.UserID = glIDSplits[1]
	case "key":
		params.KeyID = glIDSplits[1]
	default:
		return fmt.Errorf("unsupported type %s by gl_id %q", glIDSplits[0], params.GlID)
	}

	params.Repository, _ = os.LookupEnv("GL_REPOSITORY")
	if params.Repository == "" {
		cwd, err := os.Getwd()
		if err != nil {
			return fmt.Errorf("parse repository path failed: %w", err)
		}
		repoStart := strings.Index(cwd, "repositories/")
		if repoStart != -1 {
			projectPath := cwd[repoStart+len("repositories/") : len(cwd)-4]
			params.RepositoryPath = projectPath
		}
	}
	params.ProjectID, _ = os.LookupEnv("GL_REPOSITORY_ID")
	params.GlUsername, _ = os.LookupEnv("GL_USERNAME")

	return nil
}

// resolveSourceAndTargetBranch: resolve and validate target branch and try to find source branch
// TODO(moweng.xx): better logic for branches resolving, maybe put proc-receive into satellite?
func (params *AGitFlowParams) resolveSourceAndTargetBranch(ref string) error {
	targetBranch := "refs/heads"
	sourceBranch := ""
	isValid := false
	refSplits := strings.Split(ref, "/")
	for i := 2; i < len(refSplits); i++ {
		if !isValid {
			targetBranch = targetBranch + "/" + refSplits[i]
			gitCmd := fmt.Sprintf("git log %s -n 1 --oneline >/dev/null 2>&1", targetBranch)
			command := exec.Command("bash", "-c", gitCmd)
			command.Env = append(command.Env, os.Environ()...)
			err := command.Run()
			if err == nil {
				isValid = true
			}
		} else if sourceBranch == "" {
			sourceBranch = refSplits[i]
		} else {
			sourceBranch = sourceBranch + "/" + refSplits[i]
		}
	}

	if !isValid {
		return fmt.Errorf("no target branch found by %q", ref)
	}

	params.TargetBranch = strings.TrimPrefix(targetBranch, "refs/heads/")

	if strings.Contains(sourceBranch, "%") {
		sourceBranch = sourceBranch[:strings.Index(sourceBranch, "%")]
	}
	params.SourceBranch = sourceBranch
	return nil
}

func (c *CodeupCommandHandler) resolveTBDParams(ctx context.Context, command *protocol.Command, action string) (*AGitFlowParams, error) {
	params := &AGitFlowParams{
		NewOID:      command.NewOID,
		OldOID:      command.OldOID,
		IsDraft:     false,
		PushOpt:     action,
		UseSpaceX:   true,
		Description: new(string),
	}
	if action == "create" {
		if err := params.resolveSourceAndTargetBranch(command.Ref); err != nil {
			return nil, err
		}
		params.PushOpt = "new"
		params.TryUpdate = false
	}

	if err := params.resolvePushOptions(*c.pushopts); err != nil {
		return nil, err
	}

	if err := params.resolveRefOptions(command.Ref); err != nil {
		return nil, err
	}

	if err := params.resolveEnv(); err != nil {
		return nil, err
	}

	if params.UserID == "" && params.KeyID != "" {
		userInfo, err := c.api.QueryUser(ctx, params.KeyID)
		if err != nil {
			return nil, err
		}
		params.UserID = strconv.Itoa(userInfo.ID)
	}

	return params, nil
}

// resolveParams 解析环境变量和参数，在新版cr下，agit-flow 和推送评审模式可以统一
func (c *CodeupCommandHandler) resolveParams(ctx context.Context, params *AGitFlowParams, command *protocol.Command) (err error) {
	if err = params.resolveEnv(); err != nil {
		command.SetHint("resolve env failed")
	}
	if err = params.resolvePushOptions(*c.pushopts); err != nil {
		command.SetHint("resolve push option failed")
	}
	if err = params.resolveRefOptions(command.Ref); err != nil {
		command.SetHint("resolve ref option failed")
	}
	if params.Action == "create" {
		if err = params.resolveSourceAndTargetBranch(command.GitCommand.Ref); err != nil {
			command.SetHint(err.Error())
		}
	}
	if err = c.checkUser(ctx, params); err != nil {
		command.SetHint(err.Error())
	}

	return
}

func (c *CodeupCommandHandler) checkUser(ctx context.Context, params *AGitFlowParams) error {
	if params.UserID == "" && params.KeyID != "" {
		userInfo, err := c.api.QueryUser(ctx, params.KeyID)
		if err != nil {
			return fmt.Errorf("query user by %q failed", params.KeyID)
		}
		params.UserID = strconv.Itoa(userInfo.ID)
	}
	return nil
}
